<?php
/*<wikitext>
{| border=1
| <b>File</b> || TaskScheduler.php
|-
| <b>Revision</b> || $Id: TaskScheduler.php 432 2007-07-20 19:24:53Z jeanlou.dupont $
|-
| <b>Author</b> || Jean-Lou Dupont
|}<br/><br/>
 
== Purpose==
The purpose of this extension is to provide flexible scheduling services to 'job' related extensions. 
The standard MW only provides 'FIFO' scheduling with a rudimentary queue draining function. On the contrary,
this extension provides a 'calendar-based' scheduler. Standard MW 'jobs' can be scheduled.

== Features ==
* Hooks on 'ClockTickEvent' generated by 'ClockTick' extension
* Calendar based scheduling - earliest deadline first
* A 'task' is allowed to 'serialize' a state variable
** The state variable is retrieved and passed on each task run
** The state variable is kept in the database on each task run completion
* If a task class is not found or the 'run' method not accessible, the task is disabled automatically

== Dependancy ==
* StubManager extension
* ClockTick extension

== Installation ==
* Execute 'install.php' of this extension package.

== History ==

== Code ==
</wikitext>*/

// required for logging functionality.
// The stub manager also loads this file on demand - when the special page 'log' is viewed.
require_once('TaskScheduler.i18n.php');

class TaskScheduler
{
	const thisType = 'other';
	const thisName = 'TaskScheduler';
	
	//
	var $timebase;
	static $logName = 'WikiSysop';
	var $user;
	
	// database related
	static $tableName = 'task_scheduler';
	static $fields = array(
							'ts_id',
							'ts_enable',
							'ts_code',
							'ts_creation_timestamp',
							'ts_last_run_timestamp',
							'ts_next_run_timestamp',
							'ts_class',
							'ts_frequency',
							'ts_priority',
							'ts_state',
						);
	
	// error codes
	const errOK					= 0;
	const errInexistantClass	= 1;
	const errRunningTask		= 2;
	const errStarting			= 3;
	
	// state codes
	const codeEnabled			= 1;
	const codeDisabled			= 0;
	
	public function __construct()
	{
		global $wgExtensionCredits;
		$wgExtensionCredits[self::thisType][] = array( 
			'name'    => self::thisName,
			'version' => StubManager::getRevisionId('$Id: TaskScheduler.php 432 2007-07-20 19:24:53Z jeanlou.dupont $'),
			'author'  => 'Jean-Lou Dupont',
			'description' => 'Provides Task Scheduling functionality', 
		);
	}
	public function hSpecialVersionExtensionTypes( &$sp, &$extensionTypes )
	{ return true; }

	/**
		Main entry point.
		This method is called when a 'tick' event occurs.
	 */
	public function hClockTickEvent( $timebase )
	{
		return $this->run( $timebase );	
	}
	/**
	 */
	public function run( $timebase )
	{
		// User under which we will file the log entry
		$this->user = User::newFromName( self::$logName );
		
		$this->timebase = $timebase;
		
		// Get earliest deadline task
		$tasks = $this->getTasksToRun();
		
		if (!empty( $tasks ))
			foreach( $tasks as $task )
			{
				$code = $this->runTask( $task, $taskErrorCode );
				$this->updateLog( $task, $code, $taskErrorCode );

				// disable any mis-behaving tasks
				if ( $code != TaskScheduler::errOK )
					$task['ts_enable'] = TaskScheduler::codeDisabled;
					
				$task['ts_code'] = $code;
				
				$this->updateTask( $task );
			}
		
		return true;
	}
	/**
		Returns only the tasks which are scheduled to run
		at this time.
	 */
	public function getTasksToRun()
	{
		$tasks = $this->getTasks();
		
		if (empty( $tasks ))
			return null;

		$sTasks = null;

		foreach( $tasks as $task )
		{
			// check if the task is enabled.
			if (!$task['ts_enable'])
				continue;
				
			// Is this the first time this task is visited?
			if ( $task['ts_last_run_timestamp'] == 0 )
			{
				$this->updateTask( $task );	
				// don't execute it just now!
				continue;
			}
			// Is it time?
			// i.e. is in the deadline in the recent past?
			// (hopefully, not too distant ;-)
			if ($this->isTimeToRun( $task ))
				$sTasks[] = $task;
		}
				
		return $sTasks;
	}
	/**
		Returns an array of all the existing tasks.
	 */
	public function getTasks()
	{
		// Get the list of tasks
		// Order by 'next_run' timestamp
		// i.e. the earliest deadline first.

		$table	= self::$tableName;
		$fields	= self::$fields;
		$index	= 'ts_next_run_timestamp';
		
		$dbr = wfGetDB(DB_SLAVE);		
		
		$res = $dbr->select($table,
							$fields,
							null,
							__METHOD__, 
							array( 'ORDER BY' => "$index ASC")  );
		
		while ( $task = $dbr->fetchObject( $res ) )
		{
			$element = null;
			foreach( self::$fields as $field )
				$element[$field] = $task->$field;
				
			$tasks[] = $element;
		}
		
		$dbr->freeResult($res);
		
		return $tasks;
	}
	/**
			Actually executes the task through its 'run()' method.
	 */
	public function runTask( &$task, &$taskErrorCode )
	{
		$classe = $task['ts_class'];
		
		// verify that a matching class definition can be loaded.		
		global $wgAutoloadClasses;
		if ( !isset($wgAutoloadClasses[$classe] ))
			return TaskScheduler::errInexistantClass;

		// this, hopefully, should not cause any problem
		$obj = new $classe;

		// have a log entry in case something goes wrong
		// and the condition isn't caught.
		$this->updateLog( $task, TaskScheduler::errStarting, null );

		if (!is_callable( array( $obj, 'run' ) ))
			return TaskScheduler::errRunningTask;

		try
		{
			// set and get the task's state variable
			$state = $task['ts_state'];
			$taskErrorCode = $obj->run( $state );
			$task['ts_state'] = $state;
		} 
		catch( Exception $e )
		{ return TaskScheduler::errRunningTask; }
		
		return TaskScheduler::errOK;
	}
	/**
		Verifies if the task's expected run time is
		actually in the past.
	 */
	public function isTimeToRun( &$task )
	{
		// assume it is not the time to run the task.
		$code = false;
		
		$now = wfTimestampNow();
		
		// is the task expected run time in the past?
		if ( $task['ts_next_run_timestamp'] < $now )
			$code = true;

		return $code;		
	}
	/**
		Updates the 'next_run' field of the $task.
		The calculation is made based on the 'timebase'
		variable this class received from the 'ClockTickEvent' event.
	 */
	private function updateTask( &$task )
	{
		$le = $task['ts_enable'];
		$lc = $task['ts_code'];		
		$lr = wfTimestampNow(); 
		$nr = $this->calculateNextRun( $task );
		$id = $task['ts_id'];
		
		$table	= self::$tableName;
		
		// do the actual update in the db.
		$dbw = wfGetDB(DB_MASTER);

		$dbw->update( $table,
			array( /* SET */
				'ts_code'				=> $lc,
				'ts_enable'				=> $le,				
				'ts_last_run_timestamp'	=> $lr,		// set
				'ts_next_run_timestamp'	=> $nr,		// set
			),
			array( 'ts_id' => $id ),		// condition
			__METHOD__ );

		$result = $dbw->affectedRows() != 0;
		
		$dbw->commit();
		
		return $result;
	}
	/**
		Calculates the task's next run time.
	 */
	private function calculateNextRun( &$task )
	{
		// next run: ($timebase * $frequency) + current time
		
		$inc = $this->timebase * $task['ts_frequency'];
		
		return wfTimestamp( TS_MW, time() + $inc );
	}
	/**
		Adds a contextual log entry.
	 */
	private function updateLog( &$task, $code, $taskErrorCode )
	{
		static $msgMap = array(	
								TaskScheduler::errOK 				=> 'text1',
								TaskScheduler::errInexistantClass	=> 'text1',
								TaskScheduler::errRunningTask		=> 'text2',
								TaskScheduler::errStarting			=> 'text',
							);
		static $actionMap = array(
								TaskScheduler::errOK				=> 'runok',
								TaskScheduler::errInexistantClass	=> 'runfail',
								TaskScheduler::errRunningTask		=> 'runfail',
								TaskScheduler::errStarting			=> 'start',								
								);
								
		$action = $actionMap[$code];
		$msgid  = $msgMap[$code];
		$param1 = $task['ts_class'];
		$param2 = $taskErrorCode;
		
		$message = wfMsgForContent( 'schlog-'.$action.'-'.$msgid, $param1, $param2 );
		
		$log = new LogPage( 'schlog', false /*don't clog recentchanges list!*/ );
		$title = Title::makeTitle( NS_SPECIAL, 'log/schlog' );		
		$log->addEntry( $action, $title, $message );
	}

} // end class declaration
?>